# -*- coding: utf-8 -*-
#
# Copyright 2009 Vanderbilt University
# 
# Licensed under the Apache License, Version 2.0 (the "License"); 
# you may not use this file except in compliance with the License. 
# You may obtain a copy of the License at 
# 
#     http://www.apache.org/licenses/LICENSE-2.0 
# 
# Unless required by applicable law or agreed to in writing, software 
# distributed under the License is distributed on an "AS IS" BASIS, 
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
# See the License for the specific language governing permissions and 
# limitations under the License. 

"""Network API provides primitives for interacting a network.

.. moduleauthor:: John Paulett <john.paulett -at- vanderbilt.edu>
"""

from __future__ import division

import copy
import random
import networkx
import operator

from corenetwork import Node, EdgeDetail
from collections import defaultdict

    
def prune(graph, select, *args, **kw):
    """Filter the graph using select, a function that returns whether an edge 
    should be kept.  Returns a deep copy of the graph.
    """
    clone = copy_graph(graph)
    ## do not iterate and delete in same operation
    remove_edges = [edge for edge in clone.edges_iter(data=True) 
                    if not select(edge, *args, **kw)]
    for edge in remove_edges:
        remove_edge(clone, edge)
    return clone

def compare_directionality(graph):
    """Construct ratios of the inbound and outbound of rules. The supports
    of the inbound vs outbound are compared. If the graph is undirected, or 
    equally directed, then all ratios should be 1.0.
    """
    directions = {}
    donotdo = []
    for edge in graph.edges_iter(data=True):
        other = (edge[1], edge[0])
        e = edge[0:2]
        if other in donotdo:
            continue
        support = edge[2].support
        if graph.has_edge(other):
            details = graph.get_edge(other)
            other_support = details.support
            ratio = support / other_support
        else:
            ratio = float('infinity')
        donotdo.append(e)
        directions[e] = ratio
    retval = directions.items()
    retval.sort(key=operator.itemgetter(1), reverse=True)
    return retval

def reciprocity(graph, select):
    """Returns a list of all the reciprocal edges in the graph and a list of all
    the edges that match the select filter.. If the graph is
    directed, it will return both versions of that edge. If the graph is 
    undirected, only the single version of the edge will be returned.
    
    To find the reciprocity count, use `len(reciprocity(graph))`
    """
    ## only store the two nodes in a list of all nodes that match the select filter
    filtered_edges = [e[0:2] for e in graph.edges_iter(data=True) if select(e)]
    reciprocal_edges = [e for e in filtered_edges if (e[1], e[0]) in filtered_edges]
    return reciprocal_edges, filtered_edges

#TODO verify
#def satisfaction(graph):
#    """Provides a list of all the satisfied nodes in graph.
#    
#    To find the satisfaction count, use `len(satisfaction(graph))`
#    """
#    return [node for node in graph if node.satisfied()]

#TODO --do we want a k based method?
def saturation(graph, k):
    """Provides a list of all the saturated nodes in the graph.
    Saturation is defined as a node having *k* or fewer edges
    
    To find the saturated count, use len(saturation(graph))
    """
    return [node for node in graph if len(node.edges) <= k]


    
#def time_to_event(graphs):
#    edges = hornet.edge_occurrences(graphs)
#    total = len(edges)
#    times = defaultdict(int)
#    for edge, occurrences in edges.iteritems():
#        times[occurrences] += 1
#    return times

def add_edge(graph, nodeA, nodeB, **kw):
    """Creates a new edge of the form *nodeA* -> *nodeB*, adding a reference to 
    it into *graph* and always *nodeA*. If the graph is undirected, *nodeB* 
    also gets a reference to the edge. The values for this edge's EdgeDetails
    can be specified as *kw*.
    
    Returns the newly created edge.
    """
    e = create_edge(nodeA, nodeB, **kw)
    nodeA.add_edge(e)
    if not networkx.is_directed(graph):
        nodeB.add_edge(e) 
    graph.add_edge(*e)
    return e
    
def remove_edge(graph, edge):
    """Safely removes all references to the edge in the graph. The nodes lose
    their internal reference to the edge and the friend in that edge. Works for 
    both directed and undirected graphs.
    """
    nodeA, nodeB = edge[0:2]
    graph.delete_edge(nodeA, nodeB)
    nodeA.remove_edge(edge)
    
    ## If the graph is undirected
    if not networkx.is_directed:
        nodeB.remove_edge(edge)

def filter_top_friends(edge, k):
    """Method for filtering.  Returns True if the edge is within the top k 
    friends of the first node in the edge. Designed for directed graphs, 
    will return unexpected results if the graph is undirected.
    """
    node = edge[0]
    friend = edge[1]
    if friend in node.friends[0:k]:
        return True
    return False

def prune(graph, select, *args, **kw):
    """Filter the graph using select, a function that returns whether an edge 
    should be kept.  Returns a deep copy of the graph.
    """
    clone = copy_graph(graph)
    ## do not iterate and delete in same operation
    remove_edges = [edge for edge in clone.edges_iter(data=True) 
                    if not select(edge, *args, **kw)]
    for edge in remove_edges:
        remove_edge(clone, edge)
    return clone

def finalize_nodes(graph, sort):
    """Calls the finalize method on each node in the graph."""
    [node.finalize(sort=sort) for node in graph.nodes_iter()]
    
def randomize(graph):
    """Create a deep copy of the full, sorted graph, that has the neighbors of
    each node randomly sorted.
    """ 
    rgraph = copy_graph(graph)
    finalize_nodes(rgraph, sort_random)
    return rgraph